"Don't load this file if ruby isn't compiled into vim... So sad :-) "
if !has("ruby")
  finish
endif

function! Initialize_SnippetMagic()

let g:snippet_active = 0
let g:snippet_selected = 0
let g:snippet_package = ""

"Here until the end of the BLOCK is regular ruby code, but it uses the"
"VIM module to access the editor's internals."
ruby << BLOCK

require 'English'

$debug = true;
def dbg(name, var)
	if ($debug)
		File.open('c:/debug.txt', "ab+") { |f|
			f.puts(name + ': ' + var.inspect)
			f.puts()
		}
	end
end
# Define globals
$SNIP = false# General snippet class stuff
$MAGIC = false # SnippetMagic class stuff
$TAB = false # Stuff dealing with tab stop jumping
$MIR = false # Mirroring of tab stops

## Define some helper methods
def vimputs(var, s)
  s.each_line {|l| $curbuf.append($curbuf.count,l.chomp)} if var
end

# Load the snippet definitions
def load_snippets
  snippets_dir = 'C:/andrei/programs/Vim/vimfiles/_mine/snippetMagic/'
  require snippets_dir + 'snippet_set'
  sdir = File.join(snippets_dir, 'snippets')
  Dir.foreach(sdir) do |file| 
    if file =~ /\.rb$/
      fname = File.join(sdir, file) 
      SnippetSet::load(fname)
    end
  end
end
load_snippets()

# Deal with tabs & spaces
if VIM::evaluate("&expandtab") == '1'
  dbg("tab settings: expand");
  n = VIM::evaluate("&tabstop") 
  TAB = ' ' * n.to_i
else
  TAB = "\t"
end

$SNIPPET_PACKAGES = SnippetSet.all

# Define a structure to store information about active tab stops
TabStop = Struct.new(:num, :def_line, :line, :col, :text, :width, :height)

# The Snippet class is used to manage an active snippet.
class Snippet
  BACKTICK_CMD = /(`.*`)/

  def initialize(snip_string)
	dbg("snip_string: ", snip_string);
    vimputs($SNIP, "#{snip_string}")
    @snip = snip_string.split("\n") # Store as an array of lines
    @cur_tab_stop = 0
    
    # Tab stops are stored as hashes.  For each line there is an array of
    # tab-stop hashes, and those line arrays are stored in an array for the
    # whole set of tab-stops.
    @tab_stops = []
    @tab_stop_list = []
    @tab_stop_mirrors = []
  
    # Since placeholders and variables can contain placeholders
    # and variables themselves, replace them recursively by line.
    @snip = @snip.map {|line| replace_vars(line) }

    # Replace backtick expansions (code that is run as shell commands)
    # TODO: Is this a security issue?  Should any sort of check be done here?
    @snip.each {|l| l.sub!(BACKTICK_CMD, eval($1)) if l =~ BACKTICK_CMD }
    #vimputs($MAGIC, "backticked...")
    #@cur_snippet.each {|l| vimputs($MAGIC, l)}

    # Extract the tab stops from the snippet definition.  This both builds up the
    # tab-stop data structure and cleans them from the string.
    line_num = 0
    @snip = @snip.map do |line|
      col = 0
      new_line, stops = extract_stops(line, 0, line_num)

      @tab_stops[line_num] = stops # Store an array of stops per line
      line_num += 1
      new_line
    end

    # Add a default tab-stop zero at the end if one wasn't defined.
    if @tab_stop_list[0].nil?
      line = @snip.size - 1
      col = @snip[-1].size
      ts = TabStop.new(0, line, line, col, nil, 0, 1)
      @tab_stop_list[0] = ts

      if @tab_stops[line]
        @tab_stops[line] << ts
      else
        @tab_stops[line] = [ts]
      end
    end
  end
  
  # Just makes it easy to get chars from the snippet without messing with the
  # internals of the class.
  def [](i)
    return @snip[i]
  end

  def size
    return @snip.size
  end

  # Process any necessary mirroring for the recently completed tabstop.
  # So if a tabstop is in a snippet twice the first time its edited the result
  # will be duplicated at the other locations.
  def mirror_tab_stop(src, mirror, update_proc)
    # Yield out the old line, col, new height, width & text for editing
    # the buffer.
    update_proc.call(mirror.line, mirror.col, mirror.height, mirror.width, src.text)

    # Update the mirrored tabstop.  
    mirror.height = src.height
    mirror.width = src.width
    mirror.text = src.text
  end

  # After text has been edited inside a snippet all the rest of the tab stops
  # have to be updated so we can correctly move the cursor and highlight
  # regions for further editing.
  def update_tab_stops(d_line, d_col, new_text, &update_proc)
    # Get the current stop and update its text.
    ts = @tab_stop_list[@cur_tab_stop]
    ts.text = new_text

    # Determine deltas and update tabstop for good.
    dh = d_line + 1 - ts.height
    dw = new_text.size - ts.width 
    vimputs($MIR, "dw: #{dw} = #{new_text.size} - #{ts.width}")

    ts_end = ts.col + ts.width # Save the old end to find nesting
    ts.height = d_line + 1
    ts.width = new_text.size
    vimputs($MIR, "ts.width: #{ts.width}")

    # Iterate over the rest of the current line. 
    # Execute mirrors and adjust lines and columns.
    idx = @tab_stops[ts.def_line].index(ts).next
    @tab_stops[ts.def_line][idx..-1].each do |stop|
      
      # Adjust column if this stop isn't nested in the last one.
      # TODO: Work here to handle nesting.
      stop.col += dw 

      # Mirror if we run into any duplicates
      if stop.num == ts.num
        dh += ts.height - stop.height
        dw += ts.width - stop.width

        mirror_tab_stop(ts, stop, update_proc) 
      end
    end

    # Now go over the other lines.
    @tab_stops[(ts.def_line + 1)..-1].each do |stop_line|
      dw = 0
      stop_line.each do |stop| 
        vimputs($SNIP, "stop #{stop.num} l: #{stop.def_line}(+#{dh}) c: #{stop.col} (+#{dw})")
        stop.line += dh 
        stop.col += dw
        
        # Mirror if we run into any duplicates
        if stop.num == ts.num
          vimputs($MIR, "tsh: #{ts.height} sth: #{stop.height}")
          dh += ts.height - stop.height
          dw += ts.width - stop.width
          mirror_tab_stop(ts, stop, update_proc) 
        end
      end
    end
  end

  # Go to the next tab stop in the active snippet.
  def next_tab_stop
    # Find the current tab stop
    @cur_tab_stop += 1

    # For some reason zero is the last tab stop in the textmate system.
    if @cur_tab_stop == @tab_stop_list.size
      @cur_tab_stop = 0
      isLast = true
    else
      isLast = false
    end

    ts = @tab_stop_list[@cur_tab_stop]
    vimputs($SNIP, "Tab Stop: #{@cur_tab_stop} out of #{@tab_stop_list.size} (#{isLast.to_s})")
    vimputs($SNIP, "ts: #{ts.inspect}")

    return isLast, ts.line, ts.col, ts.height, ts.width
  end

  TAB_STOP = "(\\$([0-9]+))|(\\$\\{([0-9]+)\\:([^}]*)\\})"

  # Run through a snippet definition and record information about the tab stops
  # so we can quickly iterate over them while typing and tabbing.
  def extract_stops(line, offset, line_num)
    new_line = ""
    stops = []
    col = line.index(/#{TAB_STOP}/)
    if $1
      ts = TabStop.new($2.to_i, line_num, line_num, col+offset, nil, 0, 1)
      stops << ts

      @tab_stop_list[ts.num] = ts unless @tab_stop_list[ts.num]
      l, s = extract_stops($POSTMATCH, col+offset, line_num)
      new_line << $PREMATCH << l
      stops += s
    elsif $3
      # First get any nested tab stops if they exist
      l, s = extract_stops($5, col+offset, line_num)
      if s.empty?
        content = $5
      else
        content = s.last.text
      end
      ts = TabStop.new($4.to_i, line_num, line_num, col+offset, content, content.size, 1)

      @tab_stop_list[ts.num] = ts unless @tab_stop_list[ts.num]

      # Now run the rest of the line
      l2, s2 = extract_stops($POSTMATCH, col+offset+ts.width, line_num)
      new_line << $PREMATCH << l << l2
      stops += [ts] + s + s2
    else
      new_line << line
    end

    return new_line, stops 
  end

  VAR_NAME = '[A-Za-z_\-]+'
  #VARIABLE = "([^\\\\]\\$(#{VAR_NAME}))|([^\\\\]\\$\\{(#{VAR_NAME})\\:(.*?)\\})"
  VARIABLE = "([^\\\\]\\$\\{(#{VAR_NAME})\\:(.*?)\\})"

  # Replace global and environment variables present in the snippet.
  def replace_vars(line)
    # Replace $ sign variables
    if line =~ /#{VARIABLE}/i
      pre = $PREMATCH
      post = $POSTMATCH

      # Simple variable replacement
      if $1
        vimputs($MAGIC, "Var Simple: #{$1}")
        dbg("var", "Var Simple: #{$1}")
        val = get_var($2).to_s 

        # Variable replacement else default value
      else
        vimputs($MAGIC, "Var Complex: #{$2}")
        dbg("var", "Var Complex: #{$2}")
        val = get_var($4).to_s

        # If there isn't a value for this variable
        # then we need to parse the default, which could
        # contain a different variable.
        val = replace_vars($5).to_s if val.empty?
      end

      # Create the new snippet by replacing the variable with
      # its contents, and then recursively replacing other variables
      # that come after this one.
      line = $PREMATCH + val + replace_vars($POSTMATCH).to_s
    end

    line
  end

  # Get the value of a variable first looking at globals, then 
  # at environment variables.
  def get_var(varName)
    # Globals
    if global_variables.include?("$"+varName)
      val = eval("$"+varName)

      # Environment variables
    elsif ENV.include?(varName)
      val = ENV[varName]
    end 

    val
  end
end

# This is the main class for executing snippets.  It handles interaction with
# vim itself.  (Editing the buffer, moving the cursor etc.)
class SnippetMagic

  @@active_snippet = nil
  @@built_menus = false

  # Load a bundle if necessary for the current filetype, and create a new
  # instance of SnippetMagic.
  def SnippetMagic.start
      SnippetMagic.new
  end

  # Kill the currently active snippet.
  def SnippetMagic.finish
    @@active_snippet = nil
    VIM::command("let g:snippet_active = 0")
  end
  
  # Need to jump between placeholders while a snippet is active.
  def SnippetMagic.tab
    @@active_snippet.next_tab_stop 
  end

  def initialize
    @window = VIM::Window.current
    @buffer = VIM::Buffer.current
    @line_num, @col_num = @window.cursor
    dbg("@col_num", @col_num);
    @start_line_num = @last_line_num = @line_num
    @start_col_num = @col_num - 1
    @cur_line = @buffer[@line_num]
    @is_selected = false

    # Lookup the snippets according to the text to the left of the cursor
    # inside the buffer, which acts as a trigger.
    find_snippet

    # Get out fast so we don't make the user wait whenever they hit tab.
    if @cur_snippet.nil?
      @@active_snippet = nil
      VIM::command("let g:snippet_active = 0")
    else
      # Ok, we have a matching snippet!

      # We need to offset the snippet by however many columns the trigger
      # was offset.
      # TODO: Make sure this is all that's necessary.  Also look into
      # using vim's syntax stuff to deal with indentation rather than having
      # to do anything at all.
      @column_offset = @col_num - @cur_key.size
      dbg('@cur_key.size', @cur_key.size);
      dbg('column offset', @column_offset);
      @indent = @cur_line =~ /[^\s]/ # First non-whitespace char = indentation

      vimputs($MAGIC, "Column Offset: #{@column_offset}")

      # Output the snippet to the vim buffer.
      print_snippet

      # Now place the cursor in the correct location and possibly
      # highlight a placeholder word.
      isLast, line_num, col, height, width = @cur_snippet.next_tab_stop
      place_cursor(line_num, col, height, width)

      # Save the active snippet for tabbing and set the vim variable.
      if not isLast
        @@active_snippet = self 
        VIM::command("let g:snippet_active = 1")
      end

      self
    end
  end
  
  # Find the snippet that matches the current context.
  # Currently this is just looking at what's by the cursor,
  # but this will probably need to get smarter...
  def find_snippet
    col = @col_num - 1
    sk = ""
    @cur_key = nil

    ft = VIM::evaluate("&filetype")
    if (ft && $SNIPPET_PACKAGES[ft])
      set = $SNIPPET_PACKAGES[ft]
      while col >= 0 
        sk = @cur_line[col..col] + sk

        if set.snips.has_key?(sk)
          @cur_key = sk
          dbg('cur_key', @cur_key);
          # Replace tabs with whatever the TAB settings are...
          return @cur_snippet = Snippet.new(set.snips[@cur_key][1].gsub(/\t/, TAB))
        end

        col -= 1
      end
    end

    nil # Nothing if we don't find a match
  end

  # This is called each time a tab is hit during the lifetime of an active
  # snippet.  First process changes to update internal state and execute any
  # mirroring that might need to happen, and then place the cursor and possibly
  # highlight a placeholder.
  def next_tab_stop
    # Compute the deltas
    @line_num, @col_num = @window.cursor

    vimputs($TAB, "#{@line_num} #{@col_num} #{@last_line_num} #{@last_col_num}")

    # Only update stops if they have edited.
    #unless ((@line_num == @last_line_num) && (@col_num == @last_col_num))
    unless @col_num == 0 # Column is zero when tabbing out of select mode

      vimputs($TAB, "Updating because of change...")

      d_line = @line_num - @last_line_num
      d_col = @last_col_num - @col_num
      vimputs($MIR, "d_col: #{d_col} = #{@col_num} - (#{@last_col_num} + #{@column_offset})")

      # Grab the new text that has been put into this tab-stop.
      # TODO: This only works for a single line replacement, eventually this
      # should probably be built to work for multiple lines so we can do
      # multi-line mirroring.
      new_text = @buffer[@last_line_num][@last_col_num...@col_num]# + 1
      vimputs($MIR, "new_text: #{new_text}")

      # Update the stops if there has been a change from default values.
      @cur_snippet.update_tab_stops(d_line, d_col, new_text) do
        |start_line, start_col, old_height, old_width, text|
        if start_line == 0
          offset = @column_offset
        else
          offset = @indent
        end
        line = @buffer[@start_line_num + start_line]
        vimputs($MIR, "old_line:\n#{line}")
        line = line[0...start_col + offset] + text + 
          line[start_col + offset + old_width..-1]
        vimputs($MIR, "new_line:\n#{line}")
        @buffer[@start_line_num + start_line] = line
      end
    end

    # Figure out where we need to be next and save the locations.
    isLast, line_num, col, height, width = @cur_snippet.next_tab_stop

    # Move the cursor
    place_cursor(line_num, col, height, width)

    SnippetMagic.finish if isLast
  end

  # Put the cursor in its correct location for the currently active snippet,
  # and highlight a portion of the buffer if it was specified as a default
  # value in the snippet definition.
  def place_cursor(line_num, col, height, width)
    @last_line_num = line_num + @start_line_num
    if line_num == 0
      @last_col_num = @column_offset + col
    else
      @last_col_num = @indent + col
    end
    @last_width = width

    vimputs($MAGIC, "Cursor - line: #{@last_line_num} col: #{@last_col_num} width: #{width}")

    # Move the cursor and store the position
    VIM::Window.current.cursor = [@last_line_num, @last_col_num]
    
    # Setup the visual-select mode if a word needs to be highlighted.
    if width > 0
      dbg("place_cursor", "executing select on #{width}");
      VIM::command("stopinsert")
      VIM::command("normal! v#{width}l")
      #TODO: Maybe set an is_selected var to handle tabbing through selections.
    else
      vimputs($MAGIC, "no width")
      VIM::command("startinsert")
    end

  end

  # Print the formatted snippet into the buffer.
  # TODO: Figure out how to do this and make it play with damn tabs
  def print_snippet
    line_num = @line_num

    @buffer[line_num] = @cur_line[0...@column_offset] + @cur_snippet[0].chomp

    if @cur_snippet.size > 1
      @cur_snippet[1..-1].each do |line|
        @buffer.append(line_num, TAB * @indent + line.sub(/\\\$/,"$").chomp)
        line_num += 1
      end
    end
  end

end

BLOCK
endfunction
call Initialize_SnippetMagic()

" Try to execute a snippet. "
function! SnippetMagicStart()
let &selection="inclusive"
ruby << BLOCK
  SnippetMagic.start
BLOCK
endfunction
"
" reload snippets "
function! SnippetMagicReload()
ruby << BLOCK
  load_snippets()
BLOCK
endfunction

" Finish the currently active snippet. "
function! SnippetMagicFinish()
let g:snippet_active = 0
ruby << BLOCK
  SnippetMagic.finish
BLOCK
endfunction

" Remap the tab key to start snippets and run them if active. "
function! SnippetMagicTab()
if g:snippet_active 
ruby << BLOCK
  SnippetMagic.tab
BLOCK
  return ""
else
  call SnippetMagicStart()
  
  " Check again.  If a snippet was started the cursor is placed. "
  if g:snippet_active
      return "" 
  else
    return ""
  endif
endif
endfunction
imap <M-j> <c-r>=SnippetMagicTab()<cr>
vmap <M-j> gV:call SnippetMagicTab()<cr>

" Remap the escape key "
" NOTE: This nullifies active snippets. "
function! SnippetMagicEscape()
if g:snippet_active
  call SnippetMagicFinish()
endif
let &selection= "exclusive"
return ""
endfunction
inoremap <M-k> <c-r>=SnippetMagicEscape()<cr>
vnoremap <M-k> :call SnippetMagicEscape()<cr>

" Remap the up key "
" NOTE: This nullifies active snippets. "
function! SnippetMagicUp()
  if g:snippet_active
    call SnippetMagicFinish()
  endif
  return "\<up>"
endfunction
"imap <Up> normal :call SnippetMagicUp()<cr>
"vmap <Up> :call SnippetMagicUp()<cr>

" Remap the down key "
" NOTE: This nullifies active snippets. "
function! SnippetMagicDown()
if g:snippet_active
  call SnippetMagicFinish()
endif
return "\<down>"
endfunction
"imap <Down> <Esc>:call SnippetMagicDown()<cr>
"vmap <Down> :call SnippetMagicDown()<cr>
